R-Version: [Default] [32-bit] C:\Program Files\R\R-4.1.0

In folgendem Notebook werden anhand des MovieLense Datensatzes aus dem Paket RecommenderLab verschiedene Recommender erstellt. Es werden verschiedene Recommender und verschiedene Ähnlichkeiten verwendet, um diese zu vergleichen und auszuwerten. Ziel ist es, ein möglichst guter Recommender zu erstellen und zu verstehen wie dieser funktioniert. Zudem soll verstanden werden wie dieser bewertet wird und was in diesem Falle ein ‘guter’ Recommender bedeutet.

Dieses Notebook konzentriert sich auf Erkenntnisse von Auswertungen und Vergleichen. Um eine bessere Übersicht zu erhalten wurden grosse, sich widerholende Codes im Helperfile helper.R ausgelagert.

2.Binäre User-Liked-Items Matrix für alle Nutzer erzeugen.

movies_binary <- movies %>% mutate(rating = ifelse(rating > 3, 1, 0))
movies_wider <- pivot_wider(movies_binary, id_cols = user, names_from = item, values_from = rating)
rownames(movies_wider) <- movies_wider$user
Warning: Setting row names on a tibble is deprecated.
movies_wider['user'] <- NULL
user_movie_matrix <- as.matrix(movies_wider)
movies_wider

Für die Binäre User-Liked Matrix setzten wir die Grenze für ein gutes Rating bei >4. Also alle Filme, welche mit einem Rating von 3 oder weniger bewertet wurden, werden als schlecht bewertet definiert (also 0), wobei Filme mit Bewertungen von 4 oder 5 als gut bewertet definiert sind (1).

binary_non_na <- as(binarize(MovieLense, minRating = 4), 'matrix') * 1
binary_non_na[1:3, 1:3]
  Toy Story (1995) GoldenEye (1995) Four Rooms (1995)
1                1                0                 1
2                1                0                 0
3                0                0                 0

3.Dimension der User-Liked-Items Matrix prüfen und ausgeben.

dim(user_movie_matrix)
[1]  943 1664

Es sind 943 Users und 1664 bewertete Filme vorhanden. Dies entspricht der Dimension der Ursprungsdaten.

4.Movie-Genre Matrix für alle Filme erzeugen.

genres <- MovieLenseMeta
genres <- genres %>% select("title",'unknown':'Western')
rownames(genres) <- genres$title
genres['title'] <- NULL
movie_genre_matrix <- as.matrix(genres)
genres

5.Dimension der Movie-Genre Matrix prüfen und ausgeben.

dim(movie_genre_matrix)
[1] 1664   19

In dieser Matrix sind die 1664 Filme einem oder mehreren von 19 genres zugeordnet.

6.Anzahl unterschiedlicher Filmprofile bestimmen und visualisieren.

movie_genre_profile_matrix <- as.matrix(apply(movie_genre_matrix == 1, 1, 
                                       function(a) paste0(colnames(movie_genre_matrix)[a], collapse = "")))
movie_genre_profile_df <- as.data.frame(movie_genre_profile_matrix)
movie_genre_profile_df$items <- rownames(movie_genre_profile_df)

#movie_genre_profile_list <- c(unique(movie_genre_profile_df$V1))
#movie_genre_profile_list <- strsplit(movie_genre_profile_list, "\n")
#length(movie_genre_profile_list)

movie_genre_profile_df

TODO: igewie die einzelne genreprofil zelle, inen df speichere und plotte(grafik vode slides vom dani, glaub folie 13) TODO: Tipp (bitte wieder löschen): movieLenseGenres <- mutate(movieLenseMetaReduced, unknown=ifelse(unknown==1, “Unknown”, NA), Action=ifelse(Action==1, “Action”, NA), Adventure=ifelse(Adventure==1, “Adventure”, NA), Animation=ifelse(Animation==1, “Animation”, NA), Children's=ifelse(Children's==1, “Childrens”, NA), Comedy=ifelse(Comedy==1, “Comedy”, NA), Crime=ifelse(Crime==1, “Crime”, NA), Documentary=ifelse(Documentary==1, “Documentary”, NA), Drama=ifelse(Drama==1, “Drama”, NA), Fantasy=ifelse(Fantasy==1, “Fantasy”, NA), Film-Noir=ifelse(Film-Noir==1, “Film.Noir”, NA), Horror=ifelse(Horror==1, “Horror”, NA), Musical=ifelse(Musical==1, “Musical”, NA), Mystery=ifelse(Mystery==1, “Mystery”, NA), Romance=ifelse(Romance==1, “Romance”, NA), Sci-Fi=ifelse(Sci-Fi==1, “Sci.Fi”, NA), Thriller=ifelse(Thriller==1, “Thriller”, NA), War=ifelse(War==1, “War”, NA), Western=ifelse(Western==1, “Western”, NA), ) movieLenseGenres <- movieLenseGenres %>% unite(‘Combination’, unknown:Western, sep = “-”, na.rm = TRUE) movieLenseCombinations <- movieLenseGenres %>% count(Combination, sort = TRUE)

topCombinations <- head(movieLenseCombinations, 30) notTopCombimations <- tail(movieLenseCombinations, -30) topCombinations <- rbind(topCombinations, c(“Andere Kombinationen”, sum(notTopCombimations$n))) topCombinations <- mutate(topCombinations, n = as.integer(n))

ggplot(topCombinations) + geom_col(aes(x = reorder(Combination, n), y = n)) + coord_flip() + labs(title = “Verteilung der Filme nach Genre-Kombination”, subtitle = paste(“Top-30 Kombinationen von insgesamt”, nrow(movieLenseCombinations)))+ xlab(“Genre”) + ylab(“Anzahl Views”)

In dem MovieLense Datenset sind insgesamt 216 verschiedene Genreprofile von den Filmen vertreten. Das bedeitet, es sind 216 verschiedene Genrekombinationen aus den 16 gegebenen Genres entstanden. (r isch so en seich plötzlich bruuchsch nömm ‘as(data, ’data.frame)’ sondern ‘as.data.frame(data)’ WIESO????) - will as(x, ‘data.frame’) vo recommenderlab isch (nur ide vo recommenderlab gwollte Struktur funktioniert) und as.data.frame(x) vo r isch und nur for matrix-df lauft defür allgemeiner ihsetzbar isch.

movie_genre_profile_df <- movie_genre_profile_df %>%
  group_by(V1)
movie_genre_profile_df
nr_diff_movies <- binary_non_na %*% movie_genre_matrix
nr_diff_movies <- as.data.frame(nr_diff_movies)

nr_diff_movies_mean <- rownames_to_column(nr_diff_movies)

nr_diff_movies_mean <- pivot_longer(nr_diff_movies_mean, cols = !rowname, names_to = 'genre', values_to = 'count')
nr_diff_movies_mean <- nr_diff_movies_mean %>% group_by(genre) %>% summarize(count = mean(count))

nr_diff_movies

TODO: Visualisierung der verschiedener Nutzerprofile ( siehe slide 13 Daniel) In dieser Matrix ist zu sehen wie viele Filme pro genre mit mehr als 3 bewertet wurden, jeweils pro User.

nr_diff_movies_mean
nr_diff_movies_mean %>% mutate(genre = fct_reorder(genre, count)) %>% 
  ggplot(aes(x = genre, y = count)) + 
  geom_col(fill = 'steelblue') +
  coord_flip() +
  scale_y_continuous(expand = c(0,0), limits = c(0, 30)) +
  geom_text(aes(label = round(count, 2)), hjust=-0.2, color = 'black') +
  labs(
    title = "Duchschnittliche Anzahl positiv bewerteter Filme pro Genre",
    x = element_blank(), 
    y = "Anzahl",
    fill = element_blank()
  ) +
  theme_classic() + 
  theme(
    text = element_text(size = 12),
    legend.position = 'bottom'
  )

7.User-Genre-Profil Matrix mit Nutzerprofilen im Genre-Vektorraum erzeugen.


bin_user_items <- as.matrix(movies_wider)
bin_user_items[is.na(bin_user_items)] <- 0

user_genre_matrix <- bin_user_items %*% movie_genre_matrix
bin_user_items[1:5,1:5]
     Toy Story (1995) GoldenEye (1995) Four Rooms (1995) Get Shorty (1995) Copycat (1995)
[1,]                1                0                 1                 0              0
[2,]                1                0                 0                 0              0
[3,]                0                0                 0                 0              0
[4,]                0                0                 0                 0              0
[5,]                1                0                 0                 0              0

##8.Dimension der User-Genre-Profil Matrix prüfen und ausgeben.

user_genre_matrix[1:5,1:5]
     unknown Action Adventure Animation Children's
[1,]       1     39        17         5          5
[2,]       0      7         3         1          2
[3,]       0      3         2         0          0
[4,]       0      5         2         0          0
[5,]       1     23        14         8          5
dim(user_genre_matrix)
[1] 943  19

In dieser Matrix sind die Anzahl positiv bewerteter Filme pro genre für jeden User dargestellt.

9.Anzahl unterschiedlicher Nutzerprofile bestimmen, wenn Stärke der GenreKombination (a) vollständig bzw. (b) nur binär berücksichtigt wird.

user_genre_df <- as.data.frame(user_genre_matrix)
user_genre_diff <- user_genre_df %>% mutate(across(.cols = everything(), .fns = ~ifelse(.x > 0, 1, 0)))

paste("Users: ", as.character(dim(user_genre_df)[1]))
[1] "Users:  943"
paste("vollständig identisch:", as.character(dim(user_genre_df)[1] - count(distinct(user_genre_df))))
[1] "vollständig identisch: 0"
paste("binär identisch: ", as.character(dim(user_genre_df)[1] - count(distinct(user_genre_diff))))
[1] "binär identisch:  562"

Es gibt bei Betrachtung vollständiger Nutzerprofilen keine identischen Nutzerprofile. Bei binärer Betrachtung sind jedoch fast 2/3 der Nutzerprofile identisch.

Ähnlichkeit von Nutzern und Filmen

1.Cosinus-Ähnlichkeit zwischen User-Genre- und Movie-Genre-Matrix berechnen.

Test


A_test.data <- c(1.5,2.5, 1.,0.5)
A_test <- matrix(A_test.data, nrow=2)
A_test
     [,1] [,2]
[1,]  1.5  1.0
[2,]  2.5  0.5
B_test.data <- c(0.5,1., 1.5,2.)
B_test <- matrix(B_test.data, nrow=2)
B_test
     [,1] [,2]
[1,]  0.5  1.5
[2,]  1.0  2.0
result <- calc_cos_similarity_twomtrx(A_test, B_test)

if((dim(result) == dim(B_test)) && (dim(result) == dim(A_test))) {
  print("dimensions match")
} else {
  print("dimensions do not match")
}
[1] "dimensions match"
check.data <- c(0.79, 0.50, 0.87, 0.61)
check <- matrix(check.data, nrow=2)

if(max(abs(check - result)) < 1e-2){
  print("check match")
} else{
  print("check differs from result")
}
[1] "check match"

In diesem Beispiel wurde für zwei 2x2 Matrizen mit zufällig gewählten Werten die cosine similarity berechnet. Diese Berechnung wurde ebenfalls von Hand gemacht und mit der Implementierung abgegelichen. Zusätzlich wurden die Dimensionen der Inputmatrizen mit der resultierenden Matrix abgeglichen. Die Berechnung der Cosine Similarity ist somit korrekt.

Anschliessend wird die Cosine Similarity der user-genre und movie-genre Matrixberechnet. Für die Berechnung muss die zweite Matrix transponiert werden, damit für die Matrix Multiplikation die Anzahl Spalten der ersten Matrix der Anzahl Zeilen der zweiten übereinstimmt.

dim(user_genre_matrix)
[1] 943  19
dim(movie_genre_matrix)
[1] 1664   19
similarity <- calc_cos_similarity_twomtrx(user_genre_matrix, t(movie_genre_matrix))

2. Dimension der Matrix der Cosinus Ähnlichkeiten von Nutzern und Filmen prüfen uns ausgeben.

dim(similarity)
[1]  943 1664

Wie die Dimension schon erahnen lässt ist in dieser Matrix die Similarity zwischen Usern und Filmen abgebildet.

3. 5 Zahlen Statistik für Matrix der Cosinus Ähnlichkeiten prüfen uns ausgeben.

summary(c(similarity))
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
 0.0000  0.2300  0.4070  0.4098  0.5919  0.9768    1664 

Die similarities liegen zwischen 0 und 1, was bei Betrachtung der ausschliesslich positiven Bewertung Sinn ergibt. Da das Minimum bei 0 liegt, gibt es user-item Kombinationen welche keine Ähnlichkeiten aufweisen. Das Maximum von 0.9768 bedeutet, dass es sehr hohe Ähnlichkeiten zwischen user- und movie Kombinationen gibt. Interessant ist, dass 1664 NA Werte bestehen. Dies hat zu bedeuten, dass es einen User gibt, welche alle movies negativ bewertet hat.

which(is.na(similarity), arr.ind = T)[1]
[1] 685

Wie hier gezeigt, ist dies der Nutzer 685.

4. Cosinus Ähnlichkeiten von Nutzern und Filmen mit Dichteplot visualisieren.

similarity_df <- as.data.frame(similarity)
similarity_df <- pivot_longer(similarity_df, cols=colnames(similarity_df))
similarity_df
ggplot(similarity_df) +
      geom_density(aes(x = value)) +
      ggtitle("Dichteplot Cosinus Similarity zwischen user-genre und movies-genre") +
      xlab("similarity")
Warning: Removed 1664 rows containing non-finite values (stat_density).

plot_sim(similarity, "Cosinus similarity zwischen user-genre und movie-genre")

Bei der Visualisierung aller similarities wird sichtbar, dass sich die Farbunterschiede eher hoizontal ausprägen. Dies bedeutet, dass der Einfluss von Nutzern, die generell besser oder generell schlechter bewerten höher ist als die similarities zwischen den Filmen.

5. Cosinus Ähnlichkeiten von Nutzern und Filmen mit Dichteplot für Nutzer “241”, “414”, “477”, “526”, “640” und “710”

selection <- as.data.frame(similarity)[c(241, 414, 477, 526, 640, 710), ]
genres <- colnames(selection)
selection$users <- rownames(selection)
selection_long <- selection %>% pivot_longer(cols = genres)

ggplot(selection_long, aes(x=value, fill=users)) + geom_density(alpha=0.3) + 
    labs(title="Density plot Cosinus-Ähnlichkeiten ausgewählte users", subtitle="", x="similarity")

    
#plot_sim(selection, "cosine similarity user-genre and movie-genre selection")

Hier wird nochmals sichtbar, dass das die similarities pro user sehr unterschiedlich sein können. Nummer 640 hat beispielsweise eine grössere similarity Ausprägung bei etwa 0.4. Häufig sind Buckel zu erkennen, was bedeutet dass Nutzer gewisse genres besser oder schlechter bewerten.

Empfehlbare Filme

1. Bewertete Filme maskieren, d.h. “Negativabzug” der User-Items Matrix erzeugen, um anschliessend Empfehlungen herzuleiten.

movies_masked <- movies_wider
movies_masked[-1][movies_masked[-1] == 1] <- 0
movies_masked[is.na(movies_masked)] <- 1
movies_masked

2. Zeilensumme des “Negativabzuges” der User-Items Matrix für die User “5”, “25”, “50” und “150” ausgeben.

defined_user <- c(5, 25, 50, 150)
defined_user
rowSums(movies_masked[defined_user, -1])

Hier zu sehen sind die anzahl nicht bewerteter filme pro user

3. 5-Zahlen Statistik der Zeilensumme des “Negativabzuges” der User-Items Matrix bestimmen.

rowsums_masked <- rowSums(movies_masked[, -1])
summary(rowsums_masked)

Top-N Empfehlungen

1.Matrix für Bewertung aller Filme durch element-weise Multiplikation der Matrix der Cosinus-Ähnlichkeiten von Nutzern und Filmen und “Negativabzug” der User-Items Matrix erzeugen.

rating_matrix <- user_movie_matrix * movies_masked

2.Dimension der Matrix für die Bewertung aller Filme prüfen.

dim(rating_matrix)
rating_matrix

3.Top-20 Listen pro Nutzer extrahieren.

get_topn_recos <- function(rating_matrix, n){
  herdöpfel <- as(rating_matrix, 'realRatingMatrix')
  herdöpfel <- as(herdöpfel, 'data.frame')
  herdöpfel <- arrange(desc(herdöpfel$ratings)) %>%
    group_by(user) %>%
    slice(head(n)) %>%
    ungroup()
  return(herdöpfel)
}

recommendation <- get_topn_recos(rating_matrix, 20)
recommendation

4.Länge der Top-20 Listen pro Nutzer prüfen.

summary(recommendation)

##5.Verteilung der minimalen Ähnlichkeit für Top-N Listen für N = 10, 20, 50 und 100 für alle Nutzer visuell vergleichen.

analyze_topn_recos <- function(rating_matrix, list){
  recoms <- c()
  for (n in list) {
    recoms <- append(recoms, get_topn_recos(rating_matrix, n))
  }
  return(recoms)
}

analyze_topn_recos(rating_matrix, c(10, 20, 50, 100))

TODO: igewie visualisiere

##6.Top-20 Empfehlungen für Nutzer “5”, “25”, “50” und “150” visuell evaluieren.

defined_user_lists <- get_topn_recos(ratingmatrix, 20) %>%
  filter(user == c(5, 25, 50, 50, 150)) %>%
  ungroup()

defined_user_lists

TODO: clevelandplot

##7.Für Nutzer “133” und “555” Profil mit Top-N Empfehlungen für N = 20, 30, 40, 50 analysieren, visualisieren und diskutieren.

analyze_topn_recos(rating_matrix[c(133, 555),], c(20, 30, 40, 50))

TODO: Clevelandplot + diskussion

LS0tDQp0aXRsZTogIkNvbnRlbnQtYmFzZWQgUmVjb21tZW5kZXIiDQphdXRob3I6ICJQYXNjYWwgQmVyZ2VyLCBMZWEgQsO8dGxlciAmIEpvw6tsIEdyb3NqZWFuIg0Kb3V0cHV0Og0KICBodG1sX25vdGVib29rOiBkZWZhdWx0DQogIHBkZl9kb2N1bWVudDogZGVmYXVsdA0KLS0tDQpSLVZlcnNpb246ICoqW0RlZmF1bHRdIFszMi1iaXRdIEM6XFxQcm9ncmFtIEZpbGVzXFxSXFxSLTQuMS4wKioNCg0KSW4gZm9sZ2VuZGVtIE5vdGVib29rIHdlcmRlbiBhbmhhbmQgZGVzIGBNb3ZpZUxlbnNlYCBEYXRlbnNhdHplcyBhdXMgZGVtIFBha2V0IFJlY29tbWVuZGVyTGFiIHZlcnNjaGllZGVuZSBSZWNvbW1lbmRlciBlcnN0ZWxsdC4gRXMgd2VyZGVuIHZlcnNjaGllZGVuZSBSZWNvbW1lbmRlciB1bmQgdmVyc2NoaWVkZW5lIMOEaG5saWNoa2VpdGVuIHZlcndlbmRldCwgdW0gZGllc2UgenUgdmVyZ2xlaWNoZW4gdW5kIGF1c3p1d2VydGVuLiBaaWVsIGlzdCBlcywgZWluIG3DtmdsaWNoc3QgZ3V0ZXIgUmVjb21tZW5kZXIgenUgZXJzdGVsbGVuIHVuZCB6dSB2ZXJzdGVoZW4gd2llIGRpZXNlciBmdW5rdGlvbmllcnQuIFp1ZGVtIHNvbGwgdmVyc3RhbmRlbiB3ZXJkZW4gd2llIGRpZXNlciBiZXdlcnRldCB3aXJkIHVuZCB3YXMgaW4gZGllc2VtIEZhbGxlIGVpbiAnZ3V0ZXInIFJlY29tbWVuZGVyIGJlZGV1dGV0Lg0KDQpEaWVzZXMgTm90ZWJvb2sga29uemVudHJpZXJ0IHNpY2ggYXVmIEVya2VubnRuaXNzZSB2b24gQXVzd2VydHVuZ2VuIHVuZCBWZXJnbGVpY2hlbi4gVW0gZWluZSBiZXNzZXJlIMOcYmVyc2ljaHQgenUgZXJoYWx0ZW4gd3VyZGVuIGdyb3NzZSwgc2ljaCB3aWRlcmhvbGVuZGUgQ29kZXMgaW0gSGVscGVyZmlsZSBgaGVscGVyLlJgIGF1c2dlbGFnZXJ0Lg0KDQpgYGB7ciBlY2hvPUZBTFNFLCBjYWNoZT1GQUxTRSwgcmVzdWx0cz1GQUxTRSwgY29tbWVudD1GQUxTRSwgd2FybmluZz1GQUxTRX0NCiMgbsO2dGlnZSBQYWNrZXRlDQpwYWNrYWdlcyA8LSBjKCJ0aWR5dmVyc2UiLCAiZGF0YS50YWJsZSIsICJsdWJyaWRhdGUiLCAiZ2dwbG90MiIsICJnZ3RoZW1lcyIsICJyZWNvbW1lbmRlcmxhYiIsICJrbml0ciIsICdwYWxzJywgJ1JDb2xvckJyZXdlcicsICdsYXR0aWNlJywgJ2dyaWQnLCAnZ3JpZEV4dHJhJykNCg0KIyBOb2NoIG5pY2h0IGluc3RhbGxpZXJ0ZSBQYWtldGUgaW5zdGFsbGllcmVuDQppbnN0YWxsZWRfcGFja2FnZXMgPC0gcGFja2FnZXMgJWluJSByb3duYW1lcyhpbnN0YWxsZWQucGFja2FnZXMoKSkNCg0KaWYgKGFueShpbnN0YWxsZWRfcGFja2FnZXMgPT0gRkFMU0UpKSB7DQogIGluc3RhbGwucGFja2FnZXMocGFja2FnZXNbIWluc3RhbGxlZF9wYWNrYWdlc10pDQp9DQoNCiMgTGFkZW4gZGVyIFBhY2tldGUNCmludmlzaWJsZShsYXBwbHkocGFja2FnZXMsIGxpYnJhcnksIGNoYXJhY3Rlci5vbmx5ID0gVFJVRSkpDQoNCiMgSW1wb3J0aWVyZW4gdm9uIEZ1bmt0aW9uZW5lIGF1cyBoZWxwZXIgZmlsZQ0Kc291cmNlKCJoZWxwZXIuUiIpDQoNCiMgY2hhbmdlIG9wdGlvbnMNCm9wdGlvbnMoZHBseXIuc3VtbWFyaXNlLmluZm9ybSA9IEZBTFNFKQ0KDQojIC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSBkYXRhIHdyYW5nbGluZyAtLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tDQojIERhdGVuIGltcG9ydGllcmVuDQpkYXRhKE1vdmllTGVuc2UpDQoNCiMgZGF0YWZyYW1lIGVyc3RlbGxlbg0KbW92aWVzIDwtIGFzKE1vdmllTGVuc2UsICJkYXRhLmZyYW1lIikNCm1vdmllcyA8LSBtb3ZpZXMgJT4lIG11dGF0ZV9pZihpcy5jaGFyYWN0ZXIsIGFzLmZhY3RvcikNCg0KIyBicmVpdGUgdmVyc2lvbiBkZXMgZGF0YWZyYW1lIGVyc3RlbGxlbg0KIyBtb3ZpZXNfd2lkZXIgPC0gcGl2b3Rfd2lkZXIoDQojICAgbW92aWVzLA0KIyAgIGlkX2NvbHMgPSB1c2VyLA0KIyAgIG5hbWVzX2Zyb20gPSBpdGVtLA0KIyAgIHZhbHVlc19mcm9tID0gcmF0aW5nLA0KIyAgIHZhbHVlc19maWxsID0gTlVMTCwNCiMgKQ0KYGBgDQoNCiMjIDIuQmluw6RyZSBVc2VyLUxpa2VkLUl0ZW1zIE1hdHJpeCBmw7xyIGFsbGUgTnV0emVyIGVyemV1Z2VuLg0KYGBge3J9DQptb3ZpZXNfYmluYXJ5IDwtIG1vdmllcyAlPiUgbXV0YXRlKHJhdGluZyA9IGlmZWxzZShyYXRpbmcgPiAzLCAxLCAwKSkNCm1vdmllc193aWRlciA8LSBwaXZvdF93aWRlcihtb3ZpZXNfYmluYXJ5LCBpZF9jb2xzID0gdXNlciwgbmFtZXNfZnJvbSA9IGl0ZW0sIHZhbHVlc19mcm9tID0gcmF0aW5nKQ0Kcm93bmFtZXMobW92aWVzX3dpZGVyKSA8LSBtb3ZpZXNfd2lkZXIkdXNlcg0KbW92aWVzX3dpZGVyWyd1c2VyJ10gPC0gTlVMTA0KdXNlcl9tb3ZpZV9tYXRyaXggPC0gYXMubWF0cml4KG1vdmllc193aWRlcikNCm1vdmllc193aWRlcg0KYGBgDQpGw7xyIGRpZSBCaW7DpHJlIFVzZXItTGlrZWQgTWF0cml4IHNldHp0ZW4gd2lyIGRpZSBHcmVuemUgZsO8ciBlaW4gZ3V0ZXMgUmF0aW5nIGJlaSA+NC4gQWxzbyBhbGxlIEZpbG1lLCB3ZWxjaGUgbWl0IGVpbmVtIFJhdGluZyB2b24gMyBvZGVyIHdlbmlnZXIgYmV3ZXJ0ZXQgd3VyZGVuLCB3ZXJkZW4gYWxzIHNjaGxlY2h0IGJld2VydGV0IGRlZmluaWVydCAoYWxzbyAwKSwgd29iZWkgRmlsbWUgbWl0IEJld2VydHVuZ2VuIHZvbiA0IG9kZXIgNSBhbHMgZ3V0IGJld2VydGV0IGRlZmluaWVydCBzaW5kICgxKS4NCg0KDQpgYGB7cn0NCmJpbmFyeV9ub25fbmEgPC0gYXMoYmluYXJpemUoTW92aWVMZW5zZSwgbWluUmF0aW5nID0gNCksICdtYXRyaXgnKSAqIDENCmJpbmFyeV9ub25fbmFbMTozLCAxOjNdDQpgYGANCg0KIyMgMy5EaW1lbnNpb24gZGVyIFVzZXItTGlrZWQtSXRlbXMgTWF0cml4IHByw7xmZW4gdW5kIGF1c2dlYmVuLg0KYGBge3J9DQpkaW0odXNlcl9tb3ZpZV9tYXRyaXgpDQpgYGANCkVzIHNpbmQgOTQzIFVzZXJzIHVuZCAxNjY0IGJld2VydGV0ZSBGaWxtZSB2b3JoYW5kZW4uIERpZXMgZW50c3ByaWNodCBkZXIgRGltZW5zaW9uIGRlciBVcnNwcnVuZ3NkYXRlbi4NCg0KIyMgNC5Nb3ZpZS1HZW5yZSBNYXRyaXggZsO8ciBhbGxlIEZpbG1lIGVyemV1Z2VuLg0KYGBge3J9DQpnZW5yZXMgPC0gTW92aWVMZW5zZU1ldGENCmdlbnJlcyA8LSBnZW5yZXMgJT4lIHNlbGVjdCgidGl0bGUiLCd1bmtub3duJzonV2VzdGVybicpDQpyb3duYW1lcyhnZW5yZXMpIDwtIGdlbnJlcyR0aXRsZQ0KZ2VucmVzWyd0aXRsZSddIDwtIE5VTEwNCm1vdmllX2dlbnJlX21hdHJpeCA8LSBhcy5tYXRyaXgoZ2VucmVzKQ0KZ2VucmVzDQpgYGANCg0KIyMgNS5EaW1lbnNpb24gZGVyIE1vdmllLUdlbnJlIE1hdHJpeCBwcsO8ZmVuIHVuZCBhdXNnZWJlbi4NCmBgYHtyfQ0KZGltKG1vdmllX2dlbnJlX21hdHJpeCkNCmBgYA0KSW4gZGllc2VyIE1hdHJpeCBzaW5kIGRpZSAxNjY0IEZpbG1lIGVpbmVtIG9kZXIgbWVocmVyZW4gdm9uIDE5IGdlbnJlcyB6dWdlb3JkbmV0Lg0KDQoNCg0KIyMgNi5BbnphaGwgdW50ZXJzY2hpZWRsaWNoZXIgRmlsbXByb2ZpbGUgYmVzdGltbWVuIHVuZCB2aXN1YWxpc2llcmVuLg0KYGBge3J9DQptb3ZpZV9nZW5yZV9wcm9maWxlX21hdHJpeCA8LSBhcy5tYXRyaXgoYXBwbHkobW92aWVfZ2VucmVfbWF0cml4ID09IDEsIDEsIA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgZnVuY3Rpb24oYSkgcGFzdGUwKGNvbG5hbWVzKG1vdmllX2dlbnJlX21hdHJpeClbYV0sIGNvbGxhcHNlID0gIiIpKSkNCm1vdmllX2dlbnJlX3Byb2ZpbGVfZGYgPC0gYXMuZGF0YS5mcmFtZShtb3ZpZV9nZW5yZV9wcm9maWxlX21hdHJpeCkNCm1vdmllX2dlbnJlX3Byb2ZpbGVfZGYkaXRlbXMgPC0gcm93bmFtZXMobW92aWVfZ2VucmVfcHJvZmlsZV9kZikNCg0KI21vdmllX2dlbnJlX3Byb2ZpbGVfbGlzdCA8LSBjKHVuaXF1ZShtb3ZpZV9nZW5yZV9wcm9maWxlX2RmJFYxKSkNCiNtb3ZpZV9nZW5yZV9wcm9maWxlX2xpc3QgPC0gc3Ryc3BsaXQobW92aWVfZ2VucmVfcHJvZmlsZV9saXN0LCAiXG4iKQ0KI2xlbmd0aChtb3ZpZV9nZW5yZV9wcm9maWxlX2xpc3QpDQoNCm1vdmllX2dlbnJlX3Byb2ZpbGVfZGYNCmBgYA0KVE9ETzogaWdld2llIGRpZSBlaW56ZWxuZSBnZW5yZXByb2ZpbCB6ZWxsZSwgaW5lbiBkZiBzcGVpY2hlcmUgdW5kIHBsb3R0ZShncmFmaWsgdm9kZSBzbGlkZXMgdm9tIGRhbmksIGdsYXViIGZvbGllIDEzKQ0KVE9ETzogVGlwcCAoYml0dGUgd2llZGVyIGzDtnNjaGVuKToNCm1vdmllTGVuc2VHZW5yZXMgPC0gbXV0YXRlKG1vdmllTGVuc2VNZXRhUmVkdWNlZCwgdW5rbm93bj1pZmVsc2UodW5rbm93bj09MSwgIlVua25vd24iLCBOQSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBBY3Rpb249aWZlbHNlKEFjdGlvbj09MSwgIkFjdGlvbiIsIE5BKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIEFkdmVudHVyZT1pZmVsc2UoQWR2ZW50dXJlPT0xLCAiQWR2ZW50dXJlIiwgTkEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgQW5pbWF0aW9uPWlmZWxzZShBbmltYXRpb249PTEsICJBbmltYXRpb24iLCBOQSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBgQ2hpbGRyZW4nc2A9aWZlbHNlKGBDaGlsZHJlbidzYD09MSwgIkNoaWxkcmVucyIsIE5BKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIENvbWVkeT1pZmVsc2UoQ29tZWR5PT0xLCAiQ29tZWR5IiwgTkEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgQ3JpbWU9aWZlbHNlKENyaW1lPT0xLCAiQ3JpbWUiLCBOQSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBEb2N1bWVudGFyeT1pZmVsc2UoRG9jdW1lbnRhcnk9PTEsICJEb2N1bWVudGFyeSIsIE5BKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIERyYW1hPWlmZWxzZShEcmFtYT09MSwgIkRyYW1hIiwgTkEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgRmFudGFzeT1pZmVsc2UoRmFudGFzeT09MSwgIkZhbnRhc3kiLCBOQSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBgRmlsbS1Ob2lyYD1pZmVsc2UoYEZpbG0tTm9pcmA9PTEsICJGaWxtLk5vaXIiLCBOQSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBIb3Jyb3I9aWZlbHNlKEhvcnJvcj09MSwgIkhvcnJvciIsIE5BKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIE11c2ljYWw9aWZlbHNlKE11c2ljYWw9PTEsICJNdXNpY2FsIiwgTkEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgTXlzdGVyeT1pZmVsc2UoTXlzdGVyeT09MSwgIk15c3RlcnkiLCBOQSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBSb21hbmNlPWlmZWxzZShSb21hbmNlPT0xLCAiUm9tYW5jZSIsIE5BKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIGBTY2ktRmlgPWlmZWxzZShgU2NpLUZpYD09MSwgIlNjaS5GaSIsIE5BKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgIFRocmlsbGVyPWlmZWxzZShUaHJpbGxlcj09MSwgIlRocmlsbGVyIiwgTkEpLA0KICAgICAgICAgICAgICAgICAgICAgICAgICAgV2FyPWlmZWxzZShXYXI9PTEsICJXYXIiLCBOQSksDQogICAgICAgICAgICAgICAgICAgICAgICAgICBXZXN0ZXJuPWlmZWxzZShXZXN0ZXJuPT0xLCAiV2VzdGVybiIsIE5BKSwNCiAgICAgICAgICAgICAgICAgICAgICAgICAgICkNCm1vdmllTGVuc2VHZW5yZXMgPC0gbW92aWVMZW5zZUdlbnJlcyAlPiUgdW5pdGUoJ0NvbWJpbmF0aW9uJywgdW5rbm93bjpXZXN0ZXJuLCBzZXAgPSAiLSIsIG5hLnJtID0gVFJVRSkNCm1vdmllTGVuc2VDb21iaW5hdGlvbnMgPC0gbW92aWVMZW5zZUdlbnJlcyAlPiUgY291bnQoQ29tYmluYXRpb24sIHNvcnQgPSBUUlVFKQ0KDQp0b3BDb21iaW5hdGlvbnMgPC0gaGVhZChtb3ZpZUxlbnNlQ29tYmluYXRpb25zLCAzMCkNCm5vdFRvcENvbWJpbWF0aW9ucyA8LSB0YWlsKG1vdmllTGVuc2VDb21iaW5hdGlvbnMsIC0zMCkNCnRvcENvbWJpbmF0aW9ucyA8LSByYmluZCh0b3BDb21iaW5hdGlvbnMsIGMoIkFuZGVyZSBLb21iaW5hdGlvbmVuIiwgc3VtKG5vdFRvcENvbWJpbWF0aW9ucyRuKSkpDQp0b3BDb21iaW5hdGlvbnMgPC0gbXV0YXRlKHRvcENvbWJpbmF0aW9ucywgbiA9IGFzLmludGVnZXIobikpDQoNCmdncGxvdCh0b3BDb21iaW5hdGlvbnMpICsNCmdlb21fY29sKGFlcyh4ID0gcmVvcmRlcihDb21iaW5hdGlvbiwgbiksIHkgPSBuKSkgKw0KY29vcmRfZmxpcCgpICsNCmxhYnModGl0bGUgPSAiVmVydGVpbHVuZyBkZXIgRmlsbWUgbmFjaCBHZW5yZS1Lb21iaW5hdGlvbiIsIHN1YnRpdGxlID0gcGFzdGUoIlRvcC0zMCBLb21iaW5hdGlvbmVuIHZvbiBpbnNnZXNhbXQiLCBucm93KG1vdmllTGVuc2VDb21iaW5hdGlvbnMpKSkrDQp4bGFiKCJHZW5yZSIpICsgeWxhYigiQW56YWhsIFZpZXdzIikNCg0KDQoNCg0KSW4gZGVtIE1vdmllTGVuc2UgRGF0ZW5zZXQgc2luZCBpbnNnZXNhbXQgMjE2ICB2ZXJzY2hpZWRlbmUgR2VucmVwcm9maWxlIHZvbiBkZW4gRmlsbWVuIHZlcnRyZXRlbi4gRGFzIGJlZGVpdGV0LCBlcyBzaW5kIDIxNiB2ZXJzY2hpZWRlbmUgR2VucmVrb21iaW5hdGlvbmVuIGF1cyBkZW4gMTYgZ2VnZWJlbmVuIEdlbnJlcyBlbnRzdGFuZGVuLg0KKHIgaXNjaCBzbyBlbiBzZWljaCBwbMO2dHpsaWNoIGJydXVjaHNjaCBuw7ZtbSAnYXMoZGF0YSwgJ2RhdGEuZnJhbWUpJyBzb25kZXJuICdhcy5kYXRhLmZyYW1lKGRhdGEpJyBXSUVTTz8/Pz8pDQotIHdpbGwgYXMoeCwgJ2RhdGEuZnJhbWUnKSB2byByZWNvbW1lbmRlcmxhYiBpc2NoIChudXIgaWRlIHZvIHJlY29tbWVuZGVybGFiIGd3b2xsdGUgU3RydWt0dXIgZnVua3Rpb25pZXJ0KSB1bmQgYXMuZGF0YS5mcmFtZSh4KSB2byByIGlzY2ggdW5kIG51ciBmb3IgbWF0cml4LWRmIGxhdWZ0IGRlZsO8ciBhbGxnZW1laW5lciBpaHNldHpiYXIgaXNjaC4NCg0KYGBge3J9DQptb3ZpZV9nZW5yZV9wcm9maWxlX2RmIDwtIG1vdmllX2dlbnJlX3Byb2ZpbGVfZGYgJT4lDQogIGdyb3VwX2J5KFYxKQ0KbW92aWVfZ2VucmVfcHJvZmlsZV9kZg0KYGBgDQoNCmBgYHtyfQ0KbnJfZGlmZl9tb3ZpZXMgPC0gYmluYXJ5X25vbl9uYSAlKiUgbW92aWVfZ2VucmVfbWF0cml4DQpucl9kaWZmX21vdmllcyA8LSBhcy5kYXRhLmZyYW1lKG5yX2RpZmZfbW92aWVzKQ0KDQpucl9kaWZmX21vdmllc19tZWFuIDwtIHJvd25hbWVzX3RvX2NvbHVtbihucl9kaWZmX21vdmllcykNCg0KbnJfZGlmZl9tb3ZpZXNfbWVhbiA8LSBwaXZvdF9sb25nZXIobnJfZGlmZl9tb3ZpZXNfbWVhbiwgY29scyA9ICFyb3duYW1lLCBuYW1lc190byA9ICdnZW5yZScsIHZhbHVlc190byA9ICdjb3VudCcpDQpucl9kaWZmX21vdmllc19tZWFuIDwtIG5yX2RpZmZfbW92aWVzX21lYW4gJT4lIGdyb3VwX2J5KGdlbnJlKSAlPiUgc3VtbWFyaXplKGNvdW50ID0gbWVhbihjb3VudCkpDQoNCm5yX2RpZmZfbW92aWVzDQpgYGANClRPRE86IFZpc3VhbGlzaWVydW5nIGRlciB2ZXJzY2hpZWRlbmVyIE51dHplcnByb2ZpbGUgKCBzaWVoZSBzbGlkZSAxMyBEYW5pZWwpDQpJbiBkaWVzZXIgTWF0cml4IGlzdCB6dSBzZWhlbiB3aWUgdmllbGUgRmlsbWUgcHJvIGdlbnJlIG1pdCBtZWhyIGFscyAzIGJld2VydGV0IHd1cmRlbiwgamV3ZWlscyBwcm8gVXNlci4NCmBgYHtyfQ0KbnJfZGlmZl9tb3ZpZXNfbWVhbg0KYGBgDQoNCmBgYHtyfQ0KbnJfZGlmZl9tb3ZpZXNfbWVhbiAlPiUgbXV0YXRlKGdlbnJlID0gZmN0X3Jlb3JkZXIoZ2VucmUsIGNvdW50KSkgJT4lIA0KICBnZ3Bsb3QoYWVzKHggPSBnZW5yZSwgeSA9IGNvdW50KSkgKyANCiAgZ2VvbV9jb2woZmlsbCA9ICdzdGVlbGJsdWUnKSArDQogIGNvb3JkX2ZsaXAoKSArDQogIHNjYWxlX3lfY29udGludW91cyhleHBhbmQgPSBjKDAsMCksIGxpbWl0cyA9IGMoMCwgMzApKSArDQogIGdlb21fdGV4dChhZXMobGFiZWwgPSByb3VuZChjb3VudCwgMikpLCBoanVzdD0tMC4yLCBjb2xvciA9ICdibGFjaycpICsNCiAgbGFicygNCiAgICB0aXRsZSA9ICJEdWNoc2Nobml0dGxpY2hlIEFuemFobCBwb3NpdGl2IGJld2VydGV0ZXIgRmlsbWUgcHJvIEdlbnJlIiwNCiAgICB4ID0gZWxlbWVudF9ibGFuaygpLCANCiAgICB5ID0gIkFuemFobCIsDQogICAgZmlsbCA9IGVsZW1lbnRfYmxhbmsoKQ0KICApICsNCiAgdGhlbWVfY2xhc3NpYygpICsgDQogIHRoZW1lKA0KICAgIHRleHQgPSBlbGVtZW50X3RleHQoc2l6ZSA9IDEyKSwNCiAgICBsZWdlbmQucG9zaXRpb24gPSAnYm90dG9tJw0KICApDQpgYGANCg0KIyMgNy5Vc2VyLUdlbnJlLVByb2ZpbCBNYXRyaXggbWl0IE51dHplcnByb2ZpbGVuIGltIEdlbnJlLVZla3RvcnJhdW0gZXJ6ZXVnZW4uDQoNCmBgYHtyfQ0KDQpiaW5fdXNlcl9pdGVtcyA8LSBhcy5tYXRyaXgobW92aWVzX3dpZGVyKQ0KYmluX3VzZXJfaXRlbXNbaXMubmEoYmluX3VzZXJfaXRlbXMpXSA8LSAwDQoNCnVzZXJfZ2VucmVfbWF0cml4IDwtIGJpbl91c2VyX2l0ZW1zICUqJSBtb3ZpZV9nZW5yZV9tYXRyaXgNCmBgYA0KDQpgYGB7cn0NCmJpbl91c2VyX2l0ZW1zWzE6NSwxOjVdDQpgYGANCg0KDQojIzguRGltZW5zaW9uIGRlciBVc2VyLUdlbnJlLVByb2ZpbCBNYXRyaXggcHLDvGZlbiB1bmQgYXVzZ2ViZW4uDQoNCmBgYHtyfQ0KdXNlcl9nZW5yZV9tYXRyaXhbMTo1LDE6NV0NCmRpbSh1c2VyX2dlbnJlX21hdHJpeCkNCmBgYA0KSW4gZGllc2VyIE1hdHJpeCBzaW5kIGRpZSBBbnphaGwgcG9zaXRpdiBiZXdlcnRldGVyIEZpbG1lIHBybyBnZW5yZSBmw7xyIGplZGVuIFVzZXIgZGFyZ2VzdGVsbHQuDQoNCiMjIDkuQW56YWhsIHVudGVyc2NoaWVkbGljaGVyIE51dHplcnByb2ZpbGUgYmVzdGltbWVuLCB3ZW5uIFN0w6Rya2UgZGVyIEdlbnJlS29tYmluYXRpb24gKGEpIHZvbGxzdMOkbmRpZyBiencuIChiKSBudXIgYmluw6RyIGJlcsO8Y2tzaWNodGlndCB3aXJkLg0KDQoNCmBgYHtyfQ0KdXNlcl9nZW5yZV9kZiA8LSBhcy5kYXRhLmZyYW1lKHVzZXJfZ2VucmVfbWF0cml4KQ0KdXNlcl9nZW5yZV9kaWZmIDwtIHVzZXJfZ2VucmVfZGYgJT4lIG11dGF0ZShhY3Jvc3MoLmNvbHMgPSBldmVyeXRoaW5nKCksIC5mbnMgPSB+aWZlbHNlKC54ID4gMCwgMSwgMCkpKQ0KDQpwYXN0ZSgiVXNlcnM6ICIsIGFzLmNoYXJhY3RlcihkaW0odXNlcl9nZW5yZV9kZilbMV0pKQ0KcGFzdGUoInZvbGxzdMOkbmRpZyBpZGVudGlzY2g6IiwgYXMuY2hhcmFjdGVyKGRpbSh1c2VyX2dlbnJlX2RmKVsxXSAtIGNvdW50KGRpc3RpbmN0KHVzZXJfZ2VucmVfZGYpKSkpDQpwYXN0ZSgiYmluw6RyIGlkZW50aXNjaDogIiwgYXMuY2hhcmFjdGVyKGRpbSh1c2VyX2dlbnJlX2RmKVsxXSAtIGNvdW50KGRpc3RpbmN0KHVzZXJfZ2VucmVfZGlmZikpKSkNCmBgYA0KRXMgZ2lidCBiZWkgQmV0cmFjaHR1bmcgdm9sbHN0w6RuZGlnZXIgTnV0emVycHJvZmlsZW4ga2VpbmUgaWRlbnRpc2NoZW4gTnV0emVycHJvZmlsZS4NCkJlaSBiaW7DpHJlciBCZXRyYWNodHVuZyBzaW5kIGplZG9jaCBmYXN0IDIvMyBkZXIgTnV0emVycHJvZmlsZSBpZGVudGlzY2guDQoNCg0KIyMgw4RobmxpY2hrZWl0IHZvbiBOdXR6ZXJuIHVuZCBGaWxtZW4NCg0KIyMgMS5Db3NpbnVzLcOEaG5saWNoa2VpdCB6d2lzY2hlbiBVc2VyLUdlbnJlLSB1bmQgTW92aWUtR2VucmUtTWF0cml4IGJlcmVjaG5lbi4NCg0KIyMjIFRlc3QNCmBgYHtyfQ0KDQpBX3Rlc3QuZGF0YSA8LSBjKDEuNSwyLjUsIDEuLDAuNSkNCkFfdGVzdCA8LSBtYXRyaXgoQV90ZXN0LmRhdGEsIG5yb3c9MikNCkFfdGVzdA0KDQpCX3Rlc3QuZGF0YSA8LSBjKDAuNSwxLiwgMS41LDIuKQ0KQl90ZXN0IDwtIG1hdHJpeChCX3Rlc3QuZGF0YSwgbnJvdz0yKQ0KQl90ZXN0DQoNCnJlc3VsdCA8LSBjYWxjX2Nvc19zaW1pbGFyaXR5X3R3b210cngoQV90ZXN0LCBCX3Rlc3QpDQoNCmlmKChkaW0ocmVzdWx0KSA9PSBkaW0oQl90ZXN0KSkgJiYgKGRpbShyZXN1bHQpID09IGRpbShBX3Rlc3QpKSkgew0KICBwcmludCgiZGltZW5zaW9ucyBtYXRjaCIpDQp9IGVsc2Ugew0KICBwcmludCgiZGltZW5zaW9ucyBkbyBub3QgbWF0Y2giKQ0KfQ0KY2hlY2suZGF0YSA8LSBjKDAuNzksIDAuNTAsIDAuODcsIDAuNjEpDQpjaGVjayA8LSBtYXRyaXgoY2hlY2suZGF0YSwgbnJvdz0yKQ0KDQppZihtYXgoYWJzKGNoZWNrIC0gcmVzdWx0KSkgPCAxZS0yKXsNCiAgcHJpbnQoImNoZWNrIG1hdGNoIikNCn0gZWxzZXsNCiAgcHJpbnQoImNoZWNrIGRpZmZlcnMgZnJvbSByZXN1bHQiKQ0KfQ0KDQoNCmBgYA0KDQpJbiBkaWVzZW0gQmVpc3BpZWwgd3VyZGUgZsO8ciB6d2VpIDJ4MiBNYXRyaXplbiBtaXQgenVmw6RsbGlnIGdld8OkaGx0ZW4gV2VydGVuIGRpZSBjb3NpbmUgc2ltaWxhcml0eSBiZXJlY2huZXQuIERpZXNlIEJlcmVjaG51bmcgd3VyZGUgZWJlbmZhbGxzIHZvbiBIYW5kIGdlbWFjaHQgdW5kIG1pdCBkZXIgSW1wbGVtZW50aWVydW5nIGFiZ2VnZWxpY2hlbi4gWnVzw6R0emxpY2ggd3VyZGVuIGRpZSBEaW1lbnNpb25lbiBkZXIgSW5wdXRtYXRyaXplbiBtaXQgZGVyIHJlc3VsdGllcmVuZGVuIE1hdHJpeCBhYmdlZ2xpY2hlbi4gRGllIEJlcmVjaG51bmcgZGVyIENvc2luZSBTaW1pbGFyaXR5IGlzdCBzb21pdCBrb3JyZWt0Lg0KDQoNCkFuc2NobGllc3NlbmQgd2lyZCBkaWUgQ29zaW5lIFNpbWlsYXJpdHkgZGVyIHVzZXItZ2VucmUgdW5kIG1vdmllLWdlbnJlIE1hdHJpeGJlcmVjaG5ldC4gRsO8ciBkaWUgQmVyZWNobnVuZyBtdXNzIGRpZSB6d2VpdGUgTWF0cml4IHRyYW5zcG9uaWVydCB3ZXJkZW4sIGRhbWl0IGbDvHIgZGllIE1hdHJpeCBNdWx0aXBsaWthdGlvbiBkaWUgQW56YWhsIFNwYWx0ZW4gZGVyIGVyc3RlbiBNYXRyaXggZGVyIEFuemFobCBaZWlsZW4gZGVyIHp3ZWl0ZW4gw7xiZXJlaW5zdGltbXQuDQoNCmBgYHtyfQ0KZGltKHVzZXJfZ2VucmVfbWF0cml4KQ0KZGltKG1vdmllX2dlbnJlX21hdHJpeCkNCg0Kc2ltaWxhcml0eSA8LSBjYWxjX2Nvc19zaW1pbGFyaXR5X3R3b210cngodXNlcl9nZW5yZV9tYXRyaXgsIHQobW92aWVfZ2VucmVfbWF0cml4KSkNCmBgYA0KIyMgMi4gRGltZW5zaW9uIGRlciBNYXRyaXggZGVyIENvc2ludXMgw4RobmxpY2hrZWl0ZW4gdm9uIE51dHplcm4gdW5kIEZpbG1lbiBwcsO8ZmVuIHVucyBhdXNnZWJlbi4NCg0KYGBge3J9DQpkaW0oc2ltaWxhcml0eSkNCmBgYA0KDQpXaWUgZGllIERpbWVuc2lvbiBzY2hvbiBlcmFobmVuIGzDpHNzdCBpc3QgaW4gZGllc2VyIE1hdHJpeCBkaWUgU2ltaWxhcml0eSB6d2lzY2hlbiBVc2VybiB1bmQgRmlsbWVuIGFiZ2ViaWxkZXQuDQoNCiMjIDMuIDUgWmFobGVuIFN0YXRpc3RpayBmw7xyIE1hdHJpeCBkZXIgQ29zaW51cyDDhGhubGljaGtlaXRlbiBwcsO8ZmVuIHVucyBhdXNnZWJlbi4NCg0KYGBge3J9DQpzdW1tYXJ5KGMoc2ltaWxhcml0eSkpDQpgYGANCkRpZSBzaW1pbGFyaXRpZXMgbGllZ2VuIHp3aXNjaGVuIDAgdW5kIDEsIHdhcyBiZWkgQmV0cmFjaHR1bmcgZGVyIGF1c3NjaGxpZXNzbGljaCBwb3NpdGl2ZW4gQmV3ZXJ0dW5nIFNpbm4gZXJnaWJ0LiBEYSBkYXMgTWluaW11bSBiZWkgMCBsaWVndCwgZ2lidCBlcyB1c2VyLWl0ZW0gS29tYmluYXRpb25lbiB3ZWxjaGUga2VpbmUgw4RobmxpY2hrZWl0ZW4gYXVmd2Vpc2VuLiBEYXMgTWF4aW11bSB2b24gMC45NzY4IGJlZGV1dGV0LCBkYXNzIGVzIHNlaHIgaG9oZSDDhGhubGljaGtlaXRlbiB6d2lzY2hlbiB1c2VyLSB1bmQgbW92aWUgS29tYmluYXRpb25lbiBnaWJ0Lg0KSW50ZXJlc3NhbnQgaXN0LCBkYXNzIDE2NjQgTkEgV2VydGUgYmVzdGVoZW4uIERpZXMgaGF0IHp1IGJlZGV1dGVuLCBkYXNzIGVzIGVpbmVuIFVzZXIgZ2lidCwgd2VsY2hlIGFsbGUgbW92aWVzIG5lZ2F0aXYgYmV3ZXJ0ZXQgaGF0Lg0KDQoNCmBgYHtyfQ0Kd2hpY2goaXMubmEoc2ltaWxhcml0eSksIGFyci5pbmQgPSBUKVsxXQ0KYGBgDQpXaWUgaGllciBnZXplaWd0LCBpc3QgZGllcyBkZXIgTnV0emVyIDY4NS4NCg0KDQojIyA0LiBDb3NpbnVzIMOEaG5saWNoa2VpdGVuIHZvbiBOdXR6ZXJuIHVuZCBGaWxtZW4gbWl0IERpY2h0ZXBsb3QgdmlzdWFsaXNpZXJlbi4NCg0KYGBge3J9DQpzaW1pbGFyaXR5X2RmIDwtIGFzLmRhdGEuZnJhbWUoc2ltaWxhcml0eSkNCnNpbWlsYXJpdHlfZGYgPC0gcGl2b3RfbG9uZ2VyKHNpbWlsYXJpdHlfZGYsIGNvbHM9Y29sbmFtZXMoc2ltaWxhcml0eV9kZikpDQpzaW1pbGFyaXR5X2RmDQpgYGANCg0KYGBge3J9DQpnZ3Bsb3Qoc2ltaWxhcml0eV9kZikgKw0KICAgICAgZ2VvbV9kZW5zaXR5KGFlcyh4ID0gdmFsdWUpKSArDQogICAgICBnZ3RpdGxlKCJEaWNodGVwbG90IGNvc2ludXMgc2ltaWxhcml0eSB6d2lzY2hlbiB1c2VyLWdlbnJlIHVuZCBtb3ZpZXMtZ2VucmUiKSArDQogICAgICB4bGFiKCJzaW1pbGFyaXR5IikNCmBgYA0KDQoNCmBgYHtyfQ0KcGxvdF9zaW0oc2ltaWxhcml0eSwgIkNvc2ludXMgc2ltaWxhcml0eSB6d2lzY2hlbiB1c2VyLWdlbnJlIHVuZCBtb3ZpZS1nZW5yZSIpDQpgYGANCkJlaSBkZXIgVmlzdWFsaXNpZXJ1bmcgYWxsZXIgc2ltaWxhcml0aWVzIHdpcmQgc2ljaHRiYXIsIGRhc3Mgc2ljaCBkaWUgRmFyYnVudGVyc2NoaWVkZSBlaGVyIGhvaXpvbnRhbCBhdXNwcsOkZ2VuLiBEaWVzIGJlZGV1dGV0LCBkYXNzIGRlciBFaW5mbHVzcyB2b24gTnV0emVybiwgZGllIGdlbmVyZWxsIGJlc3NlciBvZGVyIGdlbmVyZWxsIHNjaGxlY2h0ZXIgYmV3ZXJ0ZW4gaMO2aGVyIGlzdCBhbHMgZGllIHNpbWlsYXJpdGllcyB6d2lzY2hlbiBkZW4gRmlsbWVuLg0KDQoNCiMjIDUuIENvc2ludXMgw4RobmxpY2hrZWl0ZW4gdm9uIE51dHplcm4gdW5kIEZpbG1lbiBtaXQgRGljaHRlcGxvdCBmw7xyIE51dHplciDigJwyNDHigJ0sIOKAnDQxNOKAnSwg4oCcNDc34oCdLCDigJw1MjbigJ0sIOKAnDY0MOKAnSB1bmQg4oCcNzEw4oCdDQoNCmBgYHtyfQ0Kc2VsZWN0aW9uIDwtIGFzLmRhdGEuZnJhbWUoc2ltaWxhcml0eSlbYygyNDEsIDQxNCwgNDc3LCA1MjYsIDY0MCwgNzEwKSwgXQ0KZ2VucmVzIDwtIGNvbG5hbWVzKHNlbGVjdGlvbikNCnNlbGVjdGlvbiR1c2VycyA8LSByb3duYW1lcyhzZWxlY3Rpb24pDQpzZWxlY3Rpb25fbG9uZyA8LSBzZWxlY3Rpb24gJT4lIHBpdm90X2xvbmdlcihjb2xzID0gZ2VucmVzKQ0KDQpnZ3Bsb3Qoc2VsZWN0aW9uX2xvbmcsIGFlcyh4PXZhbHVlLCBmaWxsPXVzZXJzKSkgKyBnZW9tX2RlbnNpdHkoYWxwaGE9MC4zKSArIA0KICAgIGxhYnModGl0bGU9IkRpY2h0ZXBsb3QgQ29zaW51cy3DhGhubGljaGtlaXRlbjogQXVzZ2V3w6RobHRlIHVzZXJzIiwgc3VidGl0bGU9IiIsIHg9InNpbWlsYXJpdHkiKQ0KICAgIA0KDQpgYGANCkhpZXIgd2lyZCBub2NobWFscyBzaWNodGJhciwgZGFzcyBkYXMgZGllIHNpbWlsYXJpdGllcyBwcm8gdXNlciBzZWhyIHVudGVyc2NoaWVkbGljaCBzZWluIGvDtm5uZW4uIE51bW1lciA2NDAgaGF0IGJlaXNwaWVsc3dlaXNlIGVpbmUgZ3LDtnNzZXJlIHNpbWlsYXJpdHkgQXVzcHLDpGd1bmcgYmVpIGV0d2EgMC40LiBIw6R1ZmlnIHNpbmQgQnVja2VsIHp1IGVya2VubmVuLCB3YXMgYmVkZXV0ZXQgZGFzcyBOdXR6ZXIgZ2V3aXNzZSBnZW5yZXMgYmVzc2VyIG9kZXIgc2NobGVjaHRlciBiZXdlcnRlbi4NCg0KDQojIyMgRW1wZmVobGJhcmUgRmlsbWUNCiMjIDEuIEJld2VydGV0ZSBGaWxtZSBtYXNraWVyZW4sIGQuaC4g4oCcTmVnYXRpdmFienVn4oCdIGRlciBVc2VyLUl0ZW1zIE1hdHJpeCBlcnpldWdlbiwgdW0gYW5zY2hsaWVzc2VuZCBFbXBmZWhsdW5nZW4gaGVyenVsZWl0ZW4uDQpgYGB7cn0NCm1vdmllc19tYXNrZWQgPC0gbW92aWVzX3dpZGVyDQptb3ZpZXNfbWFza2VkWy0xXVttb3ZpZXNfbWFza2VkWy0xXSA9PSAxXSA8LSAwDQptb3ZpZXNfbWFza2VkW2lzLm5hKG1vdmllc19tYXNrZWQpXSA8LSAxDQptb3ZpZXNfbWFza2VkDQpgYGANCg0KIyMgMi4gWmVpbGVuc3VtbWUgZGVzIOKAnE5lZ2F0aXZhYnp1Z2Vz4oCdIGRlciBVc2VyLUl0ZW1zIE1hdHJpeCBmw7xyIGRpZSBVc2VyIOKAnDXigJ0sIOKAnDI14oCdLCDigJw1MOKAnSB1bmQg4oCcMTUw4oCdIGF1c2dlYmVuLg0KYGBge3J9DQpkZWZpbmVkX3VzZXIgPC0gYyg1LCAyNSwgNTAsIDE1MCkNCmRlZmluZWRfdXNlcg0Kcm93U3Vtcyhtb3ZpZXNfbWFza2VkW2RlZmluZWRfdXNlciwgLTFdKQ0KYGBgDQpIaWVyIHp1IHNlaGVuIHNpbmQgZGllIGFuemFobCBuaWNodCBiZXdlcnRldGVyIGZpbG1lIHBybyB1c2VyDQoNCiMjIDMuIDUtWmFobGVuIFN0YXRpc3RpayBkZXIgWmVpbGVuc3VtbWUgZGVzIOKAnE5lZ2F0aXZhYnp1Z2Vz4oCdIGRlciBVc2VyLUl0ZW1zIE1hdHJpeCBiZXN0aW1tZW4uDQpgYGB7cn0NCnJvd3N1bXNfbWFza2VkIDwtIHJvd1N1bXMobW92aWVzX21hc2tlZFssIC0xXSkNCnN1bW1hcnkocm93c3Vtc19tYXNrZWQpDQpgYGANCg0KIyMgVG9wLU4gRW1wZmVobHVuZ2VuDQojIyAxLk1hdHJpeCBmw7xyIEJld2VydHVuZyBhbGxlciBGaWxtZSBkdXJjaCBlbGVtZW50LXdlaXNlIE11bHRpcGxpa2F0aW9uIGRlciBNYXRyaXggZGVyIENvc2ludXMtw4RobmxpY2hrZWl0ZW4gdm9uIE51dHplcm4gdW5kIEZpbG1lbiB1bmQg4oCcTmVnYXRpdmFienVn4oCdIGRlciBVc2VyLUl0ZW1zIE1hdHJpeCBlcnpldWdlbi4NCmBgYHtyfQ0KcmF0aW5nX21hdHJpeCA8LSB1c2VyX21vdmllX21hdHJpeCAqIG1vdmllc19tYXNrZWQNCmBgYA0KDQojIyAyLkRpbWVuc2lvbiBkZXIgTWF0cml4IGbDvHIgZGllIEJld2VydHVuZyBhbGxlciBGaWxtZSBwcsO8ZmVuLg0KYGBge3J9DQpkaW0ocmF0aW5nX21hdHJpeCkNCmBgYA0KDQpgYGB7cn0NCnJhdGluZ19tYXRyaXgNCmBgYA0KDQoNCiMjIDMuVG9wLTIwIExpc3RlbiBwcm8gTnV0emVyIGV4dHJhaGllcmVuLg0KYGBge3J9DQpnZXRfdG9wbl9yZWNvcyA8LSBmdW5jdGlvbihyYXRpbmdfbWF0cml4LCBuKXsNCiAgaGVyZMO2cGZlbCA8LSBhcyhyYXRpbmdfbWF0cml4LCAncmVhbFJhdGluZ01hdHJpeCcpDQogIGhlcmTDtnBmZWwgPC0gYXMoaGVyZMO2cGZlbCwgJ2RhdGEuZnJhbWUnKQ0KICBoZXJkw7ZwZmVsIDwtIGFycmFuZ2UoZGVzYyhoZXJkw7ZwZmVsJHJhdGluZ3MpKSAlPiUNCiAgICBncm91cF9ieSh1c2VyKSAlPiUNCiAgICBzbGljZShoZWFkKG4pKSAlPiUNCiAgICB1bmdyb3VwKCkNCiAgcmV0dXJuKGhlcmTDtnBmZWwpDQp9DQoNCnJlY29tbWVuZGF0aW9uIDwtIGdldF90b3BuX3JlY29zKHJhdGluZ19tYXRyaXgsIDIwKQ0KcmVjb21tZW5kYXRpb24NCmBgYA0KDQojIyA0LkzDpG5nZSBkZXIgVG9wLTIwIExpc3RlbiBwcm8gTnV0emVyIHByw7xmZW4uDQpgYGB7cn0NCnN1bW1hcnkocmVjb21tZW5kYXRpb24pDQpgYGANCg0KIyM1LlZlcnRlaWx1bmcgZGVyIG1pbmltYWxlbiDDhGhubGljaGtlaXQgZsO8ciBUb3AtTiBMaXN0ZW4gZsO8ciBOID0gMTAsIDIwLCA1MCB1bmQgMTAwIGbDvHIgYWxsZSBOdXR6ZXIgdmlzdWVsbCB2ZXJnbGVpY2hlbi4NCmBgYHtyfQ0KYW5hbHl6ZV90b3BuX3JlY29zIDwtIGZ1bmN0aW9uKHJhdGluZ19tYXRyaXgsIGxpc3Qpew0KICByZWNvbXMgPC0gYygpDQogIGZvciAobiBpbiBsaXN0KSB7DQogICAgcmVjb21zIDwtIGFwcGVuZChyZWNvbXMsIGdldF90b3BuX3JlY29zKHJhdGluZ19tYXRyaXgsIG4pKQ0KICB9DQogIHJldHVybihyZWNvbXMpDQp9DQoNCmFuYWx5emVfdG9wbl9yZWNvcyhyYXRpbmdfbWF0cml4LCBjKDEwLCAyMCwgNTAsIDEwMCkpDQpgYGANClRPRE86IGlnZXdpZSB2aXN1YWxpc2llcmUNCg0KIyM2LlRvcC0yMCBFbXBmZWhsdW5nZW4gZsO8ciBOdXR6ZXIg4oCcNeKAnSwg4oCcMjXigJ0sIOKAnDUw4oCdIHVuZCDigJwxNTDigJ0gdmlzdWVsbCBldmFsdWllcmVuLg0KYGBge3J9DQpkZWZpbmVkX3VzZXJfbGlzdHMgPC0gZ2V0X3RvcG5fcmVjb3MocmF0aW5nbWF0cml4LCAyMCkgJT4lDQogIGZpbHRlcih1c2VyID09IGMoNSwgMjUsIDUwLCA1MCwgMTUwKSkgJT4lDQogIHVuZ3JvdXAoKQ0KDQpkZWZpbmVkX3VzZXJfbGlzdHMNCmBgYA0KVE9ETzogY2xldmVsYW5kcGxvdA0KDQojIzcuRsO8ciBOdXR6ZXIg4oCcMTMz4oCdIHVuZCDigJw1NTXigJ0gUHJvZmlsIG1pdCBUb3AtTiBFbXBmZWhsdW5nZW4gZsO8ciBOID0gMjAsIDMwLCA0MCwgNTAgYW5hbHlzaWVyZW4sIHZpc3VhbGlzaWVyZW4gdW5kIGRpc2t1dGllcmVuLg0KYGBge3J9DQphbmFseXplX3RvcG5fcmVjb3MocmF0aW5nX21hdHJpeFtjKDEzMywgNTU1KSxdLCBjKDIwLCAzMCwgNDAsIDUwKSkNCmBgYA0KVE9ETzogQ2xldmVsYW5kcGxvdCArIGRpc2t1c3Npb24NCg==